import path from 'path';
import fs from 'fs';
import * as parse5 from "parse5";
import * as tools from '@parse5/tools'
import ejs from "ejs";
import babelParse from '@babel/parser'
import babelTraverse from '@babel/traverse'



/**
 * 
 * @returns {import('vite').Plugin}
 */
export function viteEjs() {
  let config = {}
  let entry = ''

  // 虚拟css模块
  const virtualModuleCss = {
    name: 'virtual:my-css-module.css',
    id: '\0virtual:my-css-module.css',
    code: ''
  }

  // js虚拟模块
  const virtualModuleJs = {
    name: 'virtual:my-module',
    id: '\0virtual:my-module',
    code: `import "${virtualModuleCss.name}";
    `
  }

  const virtualModuleMap = [virtualModuleCss, virtualModuleJs]

  const vid = 'virtual:my-modules'
  const orc = {
    content: `export const msg = "from virtual module"`,
  }

  return {
    name: 'vite-ejs',
    enforce: 'pre',
    configResolved(options) {
      config = options
      // console.log('config', config)
      
    },

    transformIndexHtml: {
      order: 'pre',
      handler(_, ctx) {
        const { html, styleText, scripts } = ejsCompiler(ctx.filename, config.root, config)
        virtualModuleCss.code = styleText
        let scriptText = `import "${virtualModuleCss.name}";\n`

        scripts.forEach(s=>{
          if(s.src) {
            if(entry === '') {
              entry = `${s.src}`
            }else{
              scriptText += `import "${s.src}";\n`
            }
          }else if(s.content) {
            scriptText += `\n;( function() { ${s.content} } )()`
            // scriptText += `\n; ${s.content} `
          }
        })
        virtualModuleJs.code = scriptText
        virtualModuleJs.name = `virtual:my-module-${Date.now()}`
        virtualModuleJs.id = `\0${virtualModuleJs.name}`

        const h = html.replace(/(?=<\/body>)/, () => {
          return `<script type="module" src="${entry}"></script>`
        })
        
        console.log('entry', entry)

        orc.content = `export const msg = "from virtual module${Date.now()}"`

        
        
        return h
      }
    },
    
    resolveId(id) {

      if( id == vid) {
        return '\0' + id
      }

      if (/^virtual:/.test(id)) {
        const m = virtualModuleMap.find(m=>m.name === id)
        return m.id
      }
      return null
    },

    load(id) {
      if(id === '\0' + vid) {
        return orc.content
      }

      const m = virtualModuleMap.find(m=>m.id === id)
      if(m) return m.code
      return null
    },

    transform(code, id) {
      console.log('id', id)
      if(id === config.root + entry) {
        return code + `\nimport "${virtualModuleJs.name}";
        `
      } 
      
      if(id === virtualModuleJs.id) {
        // console.log('code', code)
      }
    },

    hotUpdate(ctx) {
      console.log('ctx', ctx.modules)
      // console.log('ctx.modules', ctx.modules)
      // console.log('ctx.file', ctx.file)
    },

    handleHotUpdate({ server, modules, timestamp }) {
      console.log('modules:-----------------',modules)
      console.log(server.moduleGraph.getModuleById(virtualModuleJs.name))

      // 手动使模块失效
      // const invalidatedModules = new Set()
      // for (const mod of modules) {
      //   server.moduleGraph.invalidateModule(
      //     mod,
      //     invalidatedModules,
      //     timestamp,
      //     true
      //   )
      // }
      
    
      // server.ws.send({ type: 'full-reload' })
      // return []
    },
    
    configureServer(server) {
      server?.watcher.add('F:/项目文件/自己公司/vite-ssg/src/pages/head.ejs').on('change', (path,state) => {
        console.log('path', path)
        
        server.ws.send({ type: 'full-reload' })
      })
      
    }


  }
}

const rex = /(?<=<%-\s*include\(\s*['"]).*(?=['"]\s*\)\s*%>)/g
const rex1 = /<%-\s*include\(\s*['"].*%>/g
const rex2 = /(?<=url\(\s*['"]).*(?=['"]\s*\)\s*)/g
/**
 * 
 * @param {string} file 
 * @param {string} rootPath 
 */
export const ejsCompiler = (file, rootPath, config) => {

  let allStyles = []
  let allScripts = []

  /** @type {Record<string, {next: string[], template: string, file: string, pre: string, key: string}>} */
  const strMap = {}

  const fn1 = (entry, key, pre) => {
    const template = fs.readFileSync(entry, 'utf-8')
    strMap[entry] = {
      next: [],
      template: '',
      file: entry,
      pre,
      key
    }
    const emp1 = template.replace(rex1, (s) => {
      const src = s.match(rex)?.[0]
      if (src) {
        const key = `${Date.now()}_${Math.random()}`
        const dirname = path.dirname(entry)
        const file = path.resolve(dirname, src)
        strMap[entry].next.push(file)
        fn1(file, key, entry)
        return key
      } else {
        return s
      }
    })
    // const emp2 = parse(emp1)
    strMap[entry].template = emp1
    return emp1
  }

  function parse(code, file) {
    const dirname = path.dirname(file)
    const ast = parse5.parse(code)
    let params = {}
    tools.traverse(ast, {
      node(n) {
        if (n.tagName === 'style') {
          if(config.mode === 'development' && !tools.hasAttribute(n, 'data-ref')) {
            tools.setAttribute(n, 'data-ref', file)
            // n.childNodes.unshift(tools.createTextNode(`\n/* ${file} */`))
          }
          n.childNodes.forEach(i=>{
            i.value = i.value.replace(rex2, s=> {
              if(!path.isAbsolute(s)){
                const src1 = path.resolve(dirname, s)
                return src1.split(path.sep).join('/')
              }else{
                return s
              }
            })
          })
        }

        if (n.tagName === 'script') {
          if (n.attrs && tools.hasAttribute(n, 'aot')) {
            params = aotScript(n.childNodes[0].value, file)
            tools.removeNode(n)
          } else {
            // allScripts.push(n)
          }
        }


        if (n.attrs && tools.hasAttribute(n, 'src')) {
          const src = tools.getAttribute(n, 'src')
          if (!path.isAbsolute(src)) {
            const src1 = path.relative(rootPath, path?.join(dirname, src))
            const src2 = src1.split(path.sep).join('/')
            tools.setAttribute(n, 'src', '/' + src2)
          }
        }

      }
    })
    const html1 = parse5.serialize(ast).replace(/&lt\;/g, '<').replace(/&gt\;/g, '>')
    // console.log('html1', html1)
    const html2 = ejs.render(html1, params)
    return html2
  }

  function synthesis(entry) {

    if (strMap[entry].next.length > 0) {
      strMap[entry].next.forEach(f => synthesis(f))
    }
   
    const tp = strMap[entry].template
    // const html = ejs.render(tp)
    const html1 = parse(tp, entry)
    strMap[entry].template = html1
    if (strMap[entry].pre) {
      strMap[strMap[entry].pre].template = strMap[strMap[entry].pre].template.replace(strMap[entry].key, html1)
    }
  }

  function aotScript(code, entry) {
    const dirname = path.dirname(entry)
    const ast = babelParse.parse(code, { sourceType: 'module' })
   
    const result = []
    ast.program.body.forEach(node =>{
      if(node.type === 'VariableDeclaration') {
        node.declarations.forEach(n=>{
          if(n.init.type !== 'ArrowFunctionExpression') {
            result.push(n.id.name)
          }
        })
      }
    })
    const code1 = code + `\n return { ${result.join(', ')} }`

    const fn = new Function(code1)
    let resultCode = {}
    try {
      resultCode = fn()
    } catch (error) {
      console.error(error)
    }
    return resultCode
    
  }

  fn1(file)
  synthesis(file)



  const ast = parse5.parse(strMap[file].template)
  
  let styleText = ''
  const scripts = []

  tools.traverse(ast, {
    node(n){
      if(n.tagName === 'style') {
        const ref = tools.getAttribute(n, 'data-ref')
        n.childNodes.forEach(i=>{
          if(ref) {
            styleText += `\n/*${ref}*/\n${i.value}`
          }else{
            styleText +=  `\n${i.value}`
          }
        })
        tools.removeNode(n)
      }
      if(n.tagName === 'script') {
        const src = tools.getAttribute(n, 'src')
        const type = tools.getAttribute(n, 'type')
        if(src) {
          scripts.push({src})
        }
        n.childNodes.forEach(i=>{
          if(i.value && type !== 'text/template') {
            scripts.push({ content: i.value})
          }
        })
        if(type !== 'text/template'){

          tools.removeNode(n)
        }

      }

    }

  })



  const html = parse5.serialize(ast)

  // console.log('html', html)

  return {
    html,
    styleText,
    scripts
  }


}